package ezXpns.GUI.controllers;

import java.util.ArrayList;
import java.util.Date;
import java.util.Map.Entry;
import java.util.UUID;

import org.apache.commons.lang3.tuple.Pair;
import ezXpns.BudgetChange;
import ezXpns.TypeRegistry;
import ezXpns.GUI.BudgetChangePanel;
import ezXpns.GUI.UndoManager;
import ezXpns.GUI.UndoableAction;
import ezXpns.analysis.AggregationResultHandler;
import ezXpns.analysis.AggregationTask;
import ezXpns.analysis.transforms.*;
import ezXpns.analysis.aggregators.*;
import ezXpns.storage.LiveQueryConnection;
import ezXpns.storage.Query;
import ezXpns.storage.Storage;
import ezXpns.storage.StorageListener;
import ezXpns.storage.queries.*;
import ezXpns.utils.DateTimeUtils;


/**
 * @author A0086931X
 */

//@author A0086931X
public class BudgetChangeController {

	public BudgetChangeController(Storage<BudgetChange> storage, TypeRegistry typeRegistry) {
		this.storage = storage;
		this.typeRegistry = typeRegistry;
	}

	public void connect(BudgetChangePanel gui) {
		if(this.gui != null) return;//To crash or not to crash, that is the question
		this.gui = gui;
		pushAvailableTypes();
	}

	public void undo() {
		undoManager.undo();
	}

	public void redo() {
		undoManager.redo();
	}

	public boolean canUndo() {
		return undoManager.canUndo();
	}

	public boolean canRedo() {
		return undoManager.canRedo();
	}

	public String getNextUndoActionName() {
		return undoManager.getNextUndoActionName();
	}	

	public String getNextRedoActionName() {
		return undoManager.getNextRedoActionName();
	}
	
	// call when date is changed
	public void changeDate(Date date) {
		currentDate = date;
		
		Pair<Date, Date> range = DateTimeUtils.getDayRange(date);
		Query<BudgetChange> query = new DateRangeQuery(range.getLeft(), range.getRight());
		
		newView(query, true);
	}

	public RequestResult requestSearch(String name, String minAmountStr, 
			String maxAmountStr, Date minDate, Date maxDate, String type, String tag) {
		ArrayList<Query<BudgetChange>> queries = new ArrayList<Query<BudgetChange>>();
		if(name != null)
			queries.add(new NameQuery(name));
		
		if(minDate != null || maxDate != null) {
			minDate = minDate != null ? minDate : new Date(0);
			maxDate = maxDate != null ? maxDate : new Date(Long.MAX_VALUE);
			
			queries.add(new DateRangeQuery(minDate, maxDate));
		}
		
		if(tag != null)
			queries.add(new TagQuery(tag));
		
		if(minAmountStr != null || maxAmountStr != null) {
			Float minAmount = parseFloat(minAmountStr, 0.f);
			Float maxAmount = parseFloat(maxAmountStr, Float.MAX_VALUE);
			
			if(minAmount != null && maxAmount != null) {
				queries.add(new AmountRangeQuery(minAmount, maxAmount));
			}
			else
				return RequestResult.reject("Invalid amount");
		}
		
		if(type != null)
			queries.add(new TypeQuery(type));
		
		if(queries.isEmpty())
			return RequestResult.accept();
		else		
			return newView(new ComboQuery<>(queries), false);
	}
	
	public void closeSearch() {
		changeDate(currentDate);
	}
	
	// call when the gui wants to delete
	public void requestDeleteItem(UUID id) {
		undoManager.execute(new RemoveItemAction(id)); 
	}

	// call when gui wants to edit
	public RequestResult requestEditItem(UUID id, String name, String amount, String type, String tags, Date date) {
		BudgetChange newObj = new BudgetChange();
		//validate name
		if(name.equals("name ") || name.equals("")){
			return RequestResult.reject("Name cannot be blank");
		}
		//validate amount
		try {
			float fAmount = Float.parseFloat(amount);
			if(fAmount < 0) {
				return RequestResult.reject("Amount cannot be negative");
			}
			newObj.setAmount(fAmount);
		}
		catch(NumberFormatException e) {
			return RequestResult.reject("Amount must be a number");
		}
		
		//validate type
		if(!typeRegistry.hasType(type)) {
			RequestResult result = requestAddType(type);
			if(!result.isAccepted()) return result;
		}
		
		newObj.setName(name);
		newObj.setType(type);
		newObj.setTagsFromString(tags);
		newObj.setDate(date);

		undoManager.execute(new EditItemAction(id, newObj)); 
		return RequestResult.accept();
	}

	// call when gui wants to add
	public RequestResult requestAddItem(String name, String amount, String type, String tags) {

		BudgetChange newObj = new BudgetChange();
		
		//validate amount
		try {
			float fAmount = Float.parseFloat(amount);
			if(fAmount < 0) {
				return RequestResult.reject("Amount cannot be negative");
			}
			newObj.setAmount(fAmount);
		}
		catch(NumberFormatException e) {
			return RequestResult.reject("Amount must be a number");
		}
		
		//validate type
		if(!typeRegistry.hasType(type)) {
			type = "Other";
			//return RequestResult.reject("Invalid type");
		}
		
		if(tags.equals("insert tags ")){
			tags = "";
		}
		
		newObj.setName(name);
		newObj.setAmount(Float.parseFloat(amount));
		newObj.setType(type);		
		newObj.setTagsFromString(tags);
		newObj.setDate(currentDate);

		undoManager.execute(new AddItemAction(newObj)); 
		return RequestResult.accept();
	}
	
	

	public RequestResult requestAddType(String newType) {
		if(newType == null) return RequestResult.reject("Type cannot be null");		
		if(newType.equals("")) return RequestResult.reject("Type cannot be empty");		
		if(typeRegistry.hasType(newType)) return RequestResult.reject("Type already exists");
		
		undoManager.execute(new AddTypeAction(newType));
		return RequestResult.accept();
	}

	public RequestResult requestRemoveType(String removedType) {
		return requestEditType(removedType, "Other");
	}	

	public RequestResult requestEditType(String oldType, String newType) {
		if(oldType == null) return RequestResult.reject("Source type cannot be null");
		if(newType == null) return RequestResult.reject("Target type cannot be null");
		if(oldType.equals("")) return RequestResult.reject("Source type cannot be empty");
		if(newType.equals("")) return RequestResult.reject("Target type cannot be empty");
		
		if(!typeRegistry.hasType(oldType)) return RequestResult.reject("Source type '" + oldType + "' does not exist");
		if(oldType.equals(newType)) return RequestResult.reject("Source type and target type are the same");
		
		//change all existing object to newType
		String actionName;
		if(newType == "Other") {
			actionName = "remove type " + oldType;
		}
		else if(typeRegistry.hasType(newType)) {
			actionName = "merge type " + oldType + " into " + newType;
		}
		else {
			actionName = "rename type " + oldType + " into " + newType;
		}
		
		UndoableBatch actions = new UndoableBatch(actionName);
		for(Entry<UUID, BudgetChange> entry: storage.query(new TypeQuery(oldType))) {
			BudgetChange newItem = entry.getValue().clone();
			newItem.setType(newType);
			actions.add(new EditItemAction(entry.getKey(), newItem));
		}
		
		if(typeRegistry.hasType(newType)) {//merge/delete
			actions.add(new DeleteTypeAction(oldType));
		}
		else {//rename
			actions.add(new RenameTypeAction(oldType, newType));
		}
		
		undoManager.execute(actions);
		return RequestResult.accept();
	}

	private void pushAvailableTypes() {
		gui.setAvailableTypes(typeRegistry);
	}
	
	private RequestResult newView(Query<BudgetChange> query, boolean addable) {
		if (connection != null) {
			connection.disconnect();
		}
		
		gui.beginView(addable);
		gui.setSubTotal(0.0f);
		
		connection = storage.liveQueryMulti(query, 
			new EntryHandler(),
			AggregationTask.create(
				new AmountExtractor(),
				new Sum(),
				new AggregationResultHandler<Float> () {
					public void onNewValue(Float value) {
						gui.setSubTotal(value);
					}
				}
			)
		);
		
		gui.endView();
		
		return RequestResult.accept();
	}

	private Float parseFloat(String maxAmountStr, float defaultValue) {
		if(maxAmountStr == null) return defaultValue;
		try {
			float val = Float.parseFloat(maxAmountStr);
			if(val < 0)
				return null;
			else
				return val;
		}
		catch(NumberFormatException e) {
			return null;
		}
	}
	
	private class EntryHandler implements StorageListener<BudgetChange> {
		@Override
		public void onAdd(UUID id, BudgetChange item) {
			gui.addItem(id, item.getName(), item.getAmount(), item.getType(), item.getTagsAsString(), item.getDate());
		}

		@Override
		public void onRemove(UUID id, BudgetChange item) {
			gui.deleteItem(id);
		}

		@Override
		public void onReplace(UUID id, BudgetChange oldItem, BudgetChange newItem) {
			gui.editItem(
				id,
				newItem.getName(),
				newItem.getAmount(),
				newItem.getType(),
				newItem.getTagsAsString(),
				newItem.getDate()
			);
		}
	}
	
	private class AddItemAction implements UndoableAction {
		public AddItemAction(BudgetChange item) {
			this.item = item;
		}

		@Override
		public void execute() {
			//if the first id is no saved, the whole operation will not be repeatable
			if(id == null) {
				id = storage.add(item);
			}
			else {
				storage.addWithId(id, item);
			}
		}

		@Override
		public void undo() {
			storage.remove(id);
		}

		@Override
		public String getName() {
			return "add item";
		}

		private BudgetChange item;
		private UUID id;
	}
	
	private class RemoveItemAction implements UndoableAction {
		public RemoveItemAction(UUID id) {
			this.id = id;
		}

		@Override
		public void execute() {
			item = storage.remove(id);
		}

		@Override
		public void undo() {
			storage.addWithId(id, item);
		}

		@Override
		public String getName() {
			return "remove item";
		}
		
		private BudgetChange item;
		private UUID id;
	}
	
	private class EditItemAction implements UndoableAction {
		public EditItemAction(UUID id, BudgetChange newItem) {
			this.id = id;
			this.newItem = newItem;
		}

		@Override
		public void execute() {
			oldItem = storage.get(id);
			storage.replace(id, newItem);
		}

		@Override
		public void undo() {
			if (oldItem != null) {
				storage.replace(id, oldItem);
			}
		}

		@Override
		public String getName() {
			return "edit item";
		}
		
		private UUID id;
		private BudgetChange newItem;
		private BudgetChange oldItem;
	}
	
	private class AddTypeAction implements UndoableAction {
		public AddTypeAction(String type) {
			this.type = type;
		}

		@Override
		public void execute() {
			typeRegistry.addType(type);
			pushAvailableTypes();
		}

		@Override
		public void undo() {
			typeRegistry.removeType(type);
			pushAvailableTypes();
		}

		@Override
		public String getName() {
			return "add type ";
		}
				
		private String type;
	}

	private class RenameTypeAction implements UndoableAction {
		public RenameTypeAction(String oldType, String newType) {
			this.oldType = oldType;
			this.newType = newType;
		}
		
		@Override
		public void execute() {
			typeRegistry.renameType(oldType, newType);
			pushAvailableTypes();
		}

		@Override
		public void undo() {
			typeRegistry.renameType(newType, oldType);
			pushAvailableTypes();		
		}

		@Override
		public String getName() {
			return "rename type " + oldType + " into " + newType;
		}
					
		private String oldType;
		private String newType;
	}
	
	private class DeleteTypeAction implements UndoableAction {
		public DeleteTypeAction(String type) {
			this.type = type;
		}
		
		@Override
		public void execute() {
			typeRegistry.removeType(type);
			pushAvailableTypes();
		}

		@Override
		public void undo() {
			typeRegistry.addType(type);
			pushAvailableTypes();		
		}

		@Override
		public String getName() {
			return "delete type " + type;
		}
		
		private String type;
	}
	
	private Storage<BudgetChange> storage;
	private UndoManager undoManager = new UndoManager();
	private BudgetChangePanel gui;
	private Date currentDate;	
	private LiveQueryConnection connection;
	private TypeRegistry typeRegistry;
}
